package com.natalia.file.service.impl.minio;

import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.PrimitiveArrayUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.Multimap;
import com.google.gson.Gson;
import com.natalia.file.config.FileProperties;
import com.natalia.file.constants.FileConstants;
import com.natalia.file.entity.*;
import com.natalia.file.enums.FileEnum;
import com.natalia.file.enums.StorageEnum;
import com.natalia.file.mapper.UploadMapper;
import com.natalia.file.mapper.UploadPartMapper;
import com.natalia.file.service.FileService;
import com.natalia.file.struct.UploadStructBasic;
import com.natalia.file.util.FileInfo;
import com.natalia.file.util.MimeType;
import io.minio.*;
import io.minio.messages.Part;
import lombok.extern.slf4j.Slf4j;
import lombok.val;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.springframework.context.annotation.Lazy;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.core.io.InputStreamSource;
import org.springframework.http.*;
import org.springframework.util.Assert;
import org.springframework.web.context.request.WebRequest;

import javax.annotation.Resource;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.util.Date;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.TimeUnit;

/**
 * @Author yang
 **/
@Slf4j
public class MinioServiceImpl implements FileService {

    @Lazy
    @Resource
    MinioClient minioClient;

    @Resource
    UploadMapper uploadMapper;

    @Lazy
    @Resource
    FileProperties minioProperties;

    @Lazy
    @Resource
    BaseMinioClient baseMinioClient;

    @Resource
    UploadPartMapper uploadPartMapper;


    @Override
    public UploadResult upload(UploadInfo uploadInfo){
        String bucketName = uploadInfo.getBucketName();
        if (ObjectUtils.isEmpty(bucketName)){
            bucketName= FileConstants.DEFAULT_BUCKET;
        }
        makeBucket(bucketName,uploadInfo.isBucketPublic());
        String objectName = FileInfo.getFilename(uploadInfo.getPathPrefix(), uploadInfo.getFileName());
        String contentType = MimeType.getMimeType(FilenameUtils.getExtension(objectName));
        InputStream inputStream = new ByteArrayInputStream(uploadInfo.getContent());
        try {
            minioClient.putObject(
                    PutObjectArgs.builder()
                            .bucket(bucketName)
                            .object(objectName)
                            .contentType(contentType)
                            .stream(inputStream, inputStream.available(), -1)
                            .build());
            MinioStorageInfo storageInfo = MinioStorageInfo.builder().bucketName(bucketName).objectName(objectName).build();
            //处理文件返回信息
            UploadResult uploadResult = UploadStructBasic.INSTANCES.toUploadResult(uploadInfo);
            uploadResult.setFileExt(FilenameUtils.getExtension(objectName));
            uploadResult.setMimeType(contentType);
            uploadResult.setOriginUrl(uploadInfo.isBucketPublic()
                    ? PolicyJsonHelper.getUrl(minioProperties.getEndpoint(), uploadInfo, objectName)
                    : new Gson().toJson(storageInfo));
            return uploadResult;
        } catch (Exception e) {
            log.error("upload file error:{}", ExceptionUtils.getStackTrace(e));
        }
        return null;
    }


    @Override
    public ResponseEntity<InputStreamSource> download(String fileId, WebRequest request){
        try {
            UploadInfo uploadInfo = uploadMapper.selectOne(
                    Wrappers.lambdaQuery(UploadInfo.class).eq(UploadInfo::getFileId, fileId)
            );
            Assert.isTrue(ObjectUtil.isNotEmpty(uploadInfo), "file does not exist.");
            long lastModified = uploadInfo.getUpdatedTime().getTime();
            String eTag = String.valueOf(lastModified + uploadInfo.getFileSize());
            if (request.checkNotModified(eTag, lastModified)) {
                return ResponseEntity.status(HttpStatus.NOT_MODIFIED).build();
            }
            MinioStorageInfo info = new Gson().fromJson(fileId, MinioStorageInfo.class);
            InputStream inputStream =  minioClient.getObject(
                    GetObjectArgs.builder().bucket(info.getBucketName()).object(info.getObjectName()).build());
            if (PrimitiveArrayUtil.isNotEmpty(IOUtils.toByteArray(inputStream))) {
                return ResponseEntity.ok()
                        .eTag(eTag)
                        .lastModified(lastModified)
                        .cacheControl(CacheControl.maxAge(60, TimeUnit.SECONDS))
                        .contentType(MediaType.parseMediaType(
                                MimeType.TYPES.getOrDefault(uploadInfo.getFileExt(), MediaType.APPLICATION_OCTET_STREAM_VALUE)))
                        .body(new ByteArrayResource(IOUtils.toByteArray(inputStream)));
            } else {
                return ResponseEntity.notFound().build();
            }
        } catch (Exception e) {
            log.error("download file error:{}", ExceptionUtils.getStackTrace(e));
        }
        return ResponseEntity.notFound().build();
    }

    /**
     * 初始化分片上传
     *
     */
    @Override
    public MultipartUploadInfo createMultipartInit(MultipartInit multipartInit) {
        String filename = FileInfo.getFilename(null, multipartInit.getFileName());
        String contentType = MediaTypeFactory.getMediaType(filename).orElse(MediaType.APPLICATION_OCTET_STREAM).toString();
        Multimap<String, String> headers = HashMultimap.create();
        headers.put(FileConstants.CONTENT_TYPE, contentType);
        //组装参数
        MultipartUploadCreate initInfo = MultipartUploadCreate.builder()
                .bucketName(StringUtils.isEmpty(minioProperties.getBucketName())?
                 FileConstants.DEFAULT_BUCKET:minioProperties.getBucketName())
                .objectName(filename)
                .headers(headers)
                .build();
        String uploadId = this.createMultipartInitS3(initInfo);
        MultipartUploadInfo multipartUploadInfo = MultipartUploadInfo.builder()
                .uploadId(uploadId)
                .fileName(filename)
                .partNum(multipartInit.getPartNum())
                .build();
        this.saveUpload(contentType,uploadId,filename,multipartInit);
        return multipartUploadInfo;
    }

    /**
     * 上传分片文件
     *
     * @param uploadPartInfo
     * @return
     */
    @Override
    public MultipartUploadInfo uploadPart(UploadPartInfo uploadPartInfo) {
        try (InputStream inputStream = uploadPartInfo.getFile().getInputStream()){
            UploadPartCreate create =  UploadPartCreate.builder()
                    .bucketName(StringUtils.isEmpty(minioProperties.getBucketName())?
                     FileConstants.DEFAULT_BUCKET : minioProperties.getBucketName())
                    .objectName(uploadPartInfo.getFileName())
                    .uploadId(uploadPartInfo.getUploadId())
                    .partNumber(uploadPartInfo.getPartNumber())
                    .data(inputStream)
                    .length(uploadPartInfo.getFile().getSize())
                    .build();
            UploadPartResponse response = this.uploadPartS3(create);
            Assert.isTrue(ObjectUtils.isNotEmpty(response),"upload part failed !");
            //保存入库
            this.partInfoSave(response,create.getBucketName(),create.getObjectName());
            return  MultipartUploadInfo.builder()
                    .uploadId(response.uploadId())
                    .partNum(response.partNumber()).build();
        }catch (Exception e){
            log.error("partition uploadPart failed:{}",ExceptionUtils.getStackTrace(e));
            Thread.currentThread().interrupt();
            return null;
        }
    }

    /**
     * 查询分片信息
     * @param uploadCreate
     * @return
     */
    @Override
    public List<ShardUploadInfo> listParts(MultipartUploadCreate uploadCreate) {
        //查询s3的分片信息
        uploadCreate.setBucketName(minioProperties.getBucketName());
        Part[] parts = listPartsS3(uploadCreate);
        log.info("s3 partition list :{}",new Gson().toJson(parts));
        List<UploadPartInfo> partInfos = uploadPartMapper.selectList(Wrappers.lambdaQuery(UploadPartInfo.class)
                .eq(UploadPartInfo::getUploadId, uploadCreate.getUploadId())
                .orderByAsc(UploadPartInfo::getPartNumber));
        return UploadStructBasic.INSTANCES.uploadPartToShardUploadInfos(partInfos);
    }

    /**
     * 查询分片信息
     * @param uploadCreate
     * @return
     */
    private Part[] listPartsS3(MultipartUploadCreate uploadCreate) {
        try {
            return  baseMinioClient.listPartsAsync(
                    uploadCreate.getBucketName(),
                    uploadCreate.getRegion(),
                    uploadCreate.getObjectName(),
                    uploadCreate.getMaxParts(),
                    uploadCreate.getPartNumberMarker(),
                    uploadCreate.getUploadId(),
                    uploadCreate.getHeaders(),
                    uploadCreate.getExtraQueryParams()
            ).get().result().partList().toArray(new Part[]{});
        }catch (Exception e){
            log.error("partition list failed:{}",ExceptionUtils.getStackTrace(e));
            Thread.currentThread().interrupt();
            return new Part[0];
        }

    }


    /**
     * 合并分片文件
     * @param completeMultipart
     * @return
     */
    @Override
    public CompleteUploadInfo completeMultipart(CompleteMultipart completeMultipart) {
        val retrieved = uploadMapper.selectOne(
                Wrappers.lambdaQuery(UploadInfo.class)
                        .eq(UploadInfo::getUploadId, completeMultipart.getUploadId())
                        .eq(UploadInfo::getFileSourceName, completeMultipart.getFileName()));
            if (ObjectUtil.isNotEmpty(retrieved) &&
                    FileEnum.YES.getValue()==retrieved.getFileStatus()) {
                return CompleteUploadInfo.builder()
                        .uploadId(retrieved.getUploadId())
                        .uploadName(retrieved.getFileSourceName())
                        .url(retrieved.getFilePath())
                        .size(retrieved.getFileSize())
                        .build();
            }
            //查询分片信息
            MultipartUploadCreate uploadCreate = MultipartUploadCreate.builder()
                    .bucketName(minioProperties.getBucketName())
                    .objectName(completeMultipart.getFileName())
                    .maxParts(completeMultipart.getChunks() + 10)
                    .uploadId(completeMultipart.getUploadId())
                    .partNumberMarker(0)
                    .build();
            Part[] parts = listPartsS3(uploadCreate);
            //合并分片
            MultipartUploadCreate complete = MultipartUploadCreate.builder()
                    .bucketName(minioProperties.getBucketName())
                    .uploadId(completeMultipart.getUploadId())
                    .objectName(completeMultipart.getFileName())
                    .parts(parts)
                    .build();
            //返回结果
        CompleteUploadInfo completeInfo = Optional.ofNullable(this.completeMultipartS3(complete))
                .map(resp -> CompleteUploadInfo.builder()
                        .url(resp.region())
                        .bucket(resp.bucket())
                        .uploadName(resp.object())
                        .build())
                .orElse(new CompleteUploadInfo());
        //保存记录
        this.updateUpload(completeInfo,uploadCreate.getUploadId());
        return completeInfo;
    }

    /**
     * 创建存储bucket
     */
    public void makeBucket(String bucketName,boolean isBucketPublic) {
        try {
            boolean exists = minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build());
            if (!exists){
                minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build());
            }
            //公开访问权限
            if (isBucketPublic){
                setBucketPublic(bucketName);
            }
        } catch (Exception e) {
            log.error("make bucket error:{}", ExceptionUtils.getStackTrace(e));
        }
    }

    /**
     * 公开bucket
     */
    public void setBucketPublic(String bucketName){
        try {
            minioClient.setBucketPolicy(SetBucketPolicyArgs.builder()
                            .bucket(bucketName)
                            .config(PolicyJsonHelper.get(bucketName))
                            .build());
        } catch (Exception e) {
            log.error("bucket public failed", e);
        }
    }


    /**
     * s3化分片上传初始化
     * @return String
     */
    private String createMultipartInitS3(MultipartUploadCreate multipartUploadCreate){
        try {
            return  baseMinioClient.createMultipartUploadAsync(
                    multipartUploadCreate.getBucketName(),
                    multipartUploadCreate.getRegion(),
                    multipartUploadCreate.getObjectName(),
                    multipartUploadCreate.getHeaders(),
                    multipartUploadCreate.getExtraQueryParams()
            ).get().result().uploadId();
        }catch (Exception e) {
            log.error("partition upload init failed:{}",ExceptionUtils.getStackTrace(e));
            Thread.currentThread().interrupt();
            return null;
        }
    }

    /**
     * s3化分片上传
     * @return UploadPartResponse
     */
    private UploadPartResponse uploadPartS3(UploadPartCreate uploadPartCreate){
        try {
         return baseMinioClient.uploadPartAsync(
                uploadPartCreate.getBucketName(),
                uploadPartCreate.getRegion(),
                uploadPartCreate.getObjectName(),
                uploadPartCreate.getData(),
                uploadPartCreate.getLength(),
                uploadPartCreate.getUploadId(),
                uploadPartCreate.getPartNumber(),
                uploadPartCreate.getHeaders(),
                uploadPartCreate.getExtraQueryParams()
        ).get();
           }catch (Exception e) {
            log.error("partition upload failed:{}",ExceptionUtils.getStackTrace(e));
            Thread.currentThread().interrupt();
            return null;
        }
    }

    /**
     * @return s3 合并分片信息
     */
    private ObjectWriteResponse completeMultipartS3(MultipartUploadCreate complete){
        try {
            return baseMinioClient.completeMultipartUploadAsync(
                    complete.getBucketName(),
                    complete.getRegion(),
                    complete.getObjectName(),
                    complete.getUploadId(),
                    complete.getParts(),
                    complete.getHeaders(),
                    complete.getExtraQueryParams()
            ).get();
        }catch (Exception e) {
            log.error("complete failed:{}",ExceptionUtils.getStackTrace(e));
            Thread.currentThread().interrupt();
            return null;
        }
    }

    /**
     *  保存文件上传信息
     * @param contentType
     * @param uploadId
     * @param multipartInit
     */
    private void saveUpload( String contentType,String uploadId,String filename,MultipartInit multipartInit){
        UploadInfo uploadInfo = UploadInfo.builder()
                .fileId(IdUtil.getSnowflakeNextIdStr())
                .uploadId(uploadId)
                .fileMd5(multipartInit.getFileMd5())
                .fileName(multipartInit.getFileName())
                .fileSourceName(filename)
                .partNum(multipartInit.getPartNum())
                .createdTime(new Date())
                .updatedTime(new Date())
                .storageType(StorageEnum.MINIO.getValue())
                .fileStatus(FileEnum.NO.getValue())
                .fileExt(FilenameUtils.getExtension(multipartInit.getFileName()))
                .mimeType(contentType).build();
        uploadMapper.insert(uploadInfo);
    }

    /**
     * 保存分片信息
     * @param response
     * @param bucketName 桶名称
     * @param objectName 文件名称
     */
    private void partInfoSave(UploadPartResponse response,String bucketName,String objectName) {
        LambdaQueryWrapper<UploadPartInfo> queryWrapper = Wrappers.lambdaQuery(UploadPartInfo.class)
                .eq(UploadPartInfo::getUploadId, response.uploadId())
                .eq(UploadPartInfo::getPartNumber, response.partNumber());
        UploadPartInfo existingPartInfo = uploadPartMapper.selectOne(queryWrapper);
        if (ObjectUtils.isNotEmpty(existingPartInfo)){
            return;
        }
        MinioStorageInfo storageInfo = MinioStorageInfo.builder().bucketName(bucketName).objectName(objectName).build();
        UploadPartInfo newPartInfo = new UploadPartInfo();
        newPartInfo.setPartId(IdUtil.getSnowflakeNextIdStr());
        newPartInfo.setUploadId(response.uploadId());
        newPartInfo.setPartNumber(response.partNumber());
        newPartInfo.setFileFullPath(new Gson().toJson(storageInfo));
        newPartInfo.setCreatedTime(new Date());
        newPartInfo.setUpdatedTime(new Date());
        newPartInfo.setFileSourceName(objectName);
        uploadPartMapper.insert(newPartInfo);
    }

    /**
     * 更新文件记录
     * @param complete
     */
    private void updateUpload(CompleteUploadInfo complete,String uploadId){
        if (ObjectUtils.isEmpty(complete)){
            return;
        }
        UploadInfo uploadInfo = uploadMapper.selectOne(
                Wrappers.lambdaQuery(UploadInfo.class).eq(UploadInfo::getUploadId, uploadId)
        );
        if (ObjectUtils.isNotEmpty(uploadInfo)){
            MinioStorageInfo storageInfo = MinioStorageInfo.builder()
                    .bucketName(complete.getBucket())
                    .objectName(complete.getUploadName()).build();
            UploadInfo retrievedUploadInfo = new UploadInfo();
            retrievedUploadInfo.setFileId(uploadInfo.getFileId());
            retrievedUploadInfo.setFileStatus(FileEnum.YES.getValue());
            retrievedUploadInfo.setFileUrl(complete.getUrl());
            retrievedUploadInfo.setFilePath(new Gson().toJson(storageInfo));
            retrievedUploadInfo.setUpdatedTime(new Date());
            uploadMapper.updateById(retrievedUploadInfo);
        }
    }
}
